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

Commit b37933aa authored by Ivan Chiang's avatar Ivan Chiang Committed by Android (Google) Code Review
Browse files

Merge "Runtime apply overlay by ThemeOverlayManager" into qt-dev

parents 05712ca5 3c78f558
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -24,9 +24,11 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.om.OverlayManager;
import android.net.Uri;
import android.os.RemoteException;
import android.text.format.DateUtils;
import android.util.Log;

import com.android.documentsui.base.Lookup;
import com.android.documentsui.clipping.ClipStorage;
@@ -35,8 +37,10 @@ import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.prefs.ScopedAccessLocalPreferences;
import com.android.documentsui.queries.SearchHistoryManager;
import com.android.documentsui.roots.ProvidersCache;
import com.android.documentsui.theme.ThemeOverlayManager;

public class DocumentsApplication extends Application {
    private static final String TAG = "DocumentsApplication";
    private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;

    private ProvidersCache mProviders;
@@ -82,13 +86,25 @@ public class DocumentsApplication extends Application {
        return ((DocumentsApplication) context.getApplicationContext()).mFileTypeLookup;
    }

    private void onApplyOverlayFinish(boolean result) {
        Log.d(TAG, "OverlayManager.setEnabled() result: " + result);
    }

    @Override
    public void onCreate() {
        super.onCreate();

        final ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        final OverlayManager om = getSystemService(OverlayManager.class);
        final int memoryClassBytes = am.getMemoryClass() * 1024 * 1024;

        if (om != null) {
            new ThemeOverlayManager(om, getPackageName()).applyOverlays(this, true,
                    this::onApplyOverlayFinish);
        } else {
            Log.w(TAG, "Can't obtain OverlayManager from System Service!");
        }

        mProviders = new ProvidersCache(this);
        mProviders.updateAsync(false);

+113 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.documentsui.theme;

import android.content.Context;
import android.content.om.OverlayInfo;
import android.content.om.OverlayManager;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;

import java.util.List;

/**
 * ThemeOverlayManager manage runtime resource overlay packages of DocumentsUI
 */
public class ThemeOverlayManager {
    private static final String TAG = ThemeOverlayManager.class.getSimpleName();
    private static final String PERMISSION_CHANGE_OVERLAY_PACKAGES =
            "android.permission.CHANGE_OVERLAY_PACKAGES";

    private final OverlayManager mOverlayManager;
    private String mTargetPackageId;
    private UserHandle mUserHandle;

    public ThemeOverlayManager(@NonNull OverlayManager overlayManager, String targetPackageId) {
        mOverlayManager = overlayManager;
        mTargetPackageId = targetPackageId;
        mUserHandle = UserHandle.of(UserHandle.myUserId());
    }

    /**
     * Apply runtime overlay package, dynamic enabled overlay do not support priority yet
     *
     * @param context the activity or context from caller
     * @param enabled whether or not enable overlay package
     */
    public void applyOverlays(Context context, boolean enabled, Consumer<Boolean> callback) {
        if (ContextCompat.checkSelfPermission(context, PERMISSION_CHANGE_OVERLAY_PACKAGES)
                == PackageManager.PERMISSION_GRANTED) {
            setEnabled(enabled, callback);
        } else {
            Log.w(TAG, "Permission: " + PERMISSION_CHANGE_OVERLAY_PACKAGES + " did not granted!");
            callback.accept(false);
        }
    }

    private List<OverlayInfo> getOverlayInfo() {
        // (b/132933212): Only static overlay package support priority attrs
        // TODO: Alternative way to support enabled multiple overlay packages by priority is
        //       tag meta-data in the application of overlay package's AndroidManifest.xml
        // TODO: Parse meta data through PM in DocumentsApplication and use collection to reorder
        return mOverlayManager.getOverlayInfosForTarget(mTargetPackageId, mUserHandle);
    }

    private void setEnabled(boolean enabled, Consumer<Boolean> callback) {
        new AsyncTask<Void, Void, Boolean>() {
            @Override
            protected Boolean doInBackground(Void... params) {
                return setEnabledOverlayPackages(getOverlayInfo(), enabled);
            }

            @Override
            protected void onPostExecute(Boolean result) {
                super.onPostExecute(result);
                if (callback != null) {
                    callback.accept(result);
                }
            }
        }.execute();
    }

    private boolean setEnabledOverlayPackages(List<OverlayInfo> infos, boolean enabled) {
        boolean bSuccess = true;
        for (OverlayInfo info : infos) {
            try {
                if (info != null && !TextUtils.isEmpty(info.getPackageName())
                        && info.isEnabled() != enabled) {
                    mOverlayManager.setEnabled(info.getPackageName(), enabled, mUserHandle);
                } else {
                    Log.w(TAG, "Skip enabled overlay package:" + info.getPackageName()
                            + ", user:" + mUserHandle);
                    bSuccess = false;
                }
            } catch (RuntimeException re) {
                Log.e(TAG, "Failed to enable overlay: " + info.getPackageName() + ", user: "
                        + mUserHandle);
                bSuccess = false;
            }
        }
        return bSuccess;
    }
}
+204 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.documentsui.theme;

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

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.om.OverlayInfo;
import android.content.om.OverlayManager;
import android.os.UserHandle;

import androidx.core.util.Consumer;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import com.google.common.collect.Lists;

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

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class ThemeOverlayManagerTest {
    private static final String TEST_DISABLED_PREFIX = "com.example.";
    private static final String TEST_ENABLED_PREFIX = "com.example.enabled.";
    private static final String TEST_OVERLAY_PACKAGE = "test.overlay";
    private static final String TEST_TARGET_PACKAGE = "test.target";

    @Mock
    OverlayManager mOverlayManager;

    Consumer<Boolean> mCallback;
    Context mContext;
    CountDownLatch mLatch;
    ThemeOverlayManager mThemeOverlayManager;
    UserHandle mUserHandle;

    @Before
    public void setUp() throws Exception {
        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
                "android.permission.CHANGE_OVERLAY_PACKAGES");

        MockitoAnnotations.initMocks(this);
        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
        mLatch = new CountDownLatch(1);
        mUserHandle = UserHandle.of(UserHandle.myUserId());
        mCallback = result -> mLatch.countDown();

        when(mOverlayManager.getOverlayInfosForTarget(getEnabledTargetPackageId(),
                mUserHandle)).thenReturn(Lists.newArrayList(
                createOverlayInfo(getOverlayPackageId(), getEnabledTargetPackageId(), true)));

        when(mOverlayManager.getOverlayInfosForTarget(getDisabledTargetPackageId(),
                mUserHandle)).thenReturn(Lists.newArrayList(
                createOverlayInfo(getOverlayPackageId(), getDisabledTargetPackageId(), false)));
    }

    @Test
    public void testOverlayPackagesForDocumentsUI_shouldBeNonStatic() {
        final String docsuiPkgId = mContext.getPackageName();
        final OverlayManager manager = mContext.getSystemService(OverlayManager.class);
        final List<OverlayInfo> infos = manager.getOverlayInfosForTarget(docsuiPkgId, mUserHandle);

        for (OverlayInfo info : infos) {
            assertThat(info.isStatic).isFalse();
        }
    }

    @Test
    public void testApplyOverlays_shouldSetEnabled() throws Exception {
        final boolean enabled = true;

        mThemeOverlayManager = new ThemeOverlayManager(mOverlayManager,
                getDisabledTargetPackageId());

        mThemeOverlayManager.applyOverlays(mContext, enabled, mCallback);
        mLatch.await(5, TimeUnit.SECONDS);

        verify(mOverlayManager, times(1)).setEnabled(getOverlayPackageId(), enabled,
                mUserHandle);
    }

    @Test
    public void testApplyOverlays_shouldGetOverlayInfo() throws Exception {
        mThemeOverlayManager = new ThemeOverlayManager(mOverlayManager,
                getEnabledTargetPackageId());

        mThemeOverlayManager.applyOverlays(mContext, true /* enabled */, mCallback);
        mLatch.await(5, TimeUnit.SECONDS);

        verify(mOverlayManager, times(1)).getOverlayInfosForTarget(getEnabledTargetPackageId(),
                mUserHandle);
    }

    @Test
    public void testApplyOverlays_shouldCheckEnabled_beforeSetEnabled() {
        final boolean enabled = true;

        mThemeOverlayManager = new ThemeOverlayManager(mOverlayManager,
                getEnabledTargetPackageId());

        mThemeOverlayManager.applyOverlays(mContext, enabled, mCallback);

        verify(mOverlayManager, never()).setEnabled(getOverlayPackageId(), enabled,
                mUserHandle);
    }

    @Test
    public void testDefaultDisabled_applyOverlays_shouldEnabled() throws Exception {
        final boolean enabled = true;

        assertThat(mOverlayManager.getOverlayInfosForTarget(getDisabledTargetPackageId(),
                mUserHandle).get(0).isEnabled()).isEqualTo(!enabled);

        mThemeOverlayManager = new ThemeOverlayManager(mOverlayManager,
                getDisabledTargetPackageId());

        mThemeOverlayManager.applyOverlays(mContext, enabled, mCallback);
        mLatch.await(5, TimeUnit.SECONDS);

        verify(mOverlayManager, times(1)).setEnabled(getOverlayPackageId(), enabled,
                mUserHandle);
    }

    @Test
    public void testDefaultEnabled_applyOverlays_shouldDisabled() throws Exception {
        final boolean enabled = false;

        assertThat(mOverlayManager.getOverlayInfosForTarget(getEnabledTargetPackageId(),
                mUserHandle).get(0).isEnabled()).isEqualTo(!enabled);

        mThemeOverlayManager = new ThemeOverlayManager(mOverlayManager,
                getEnabledTargetPackageId());

        mThemeOverlayManager.applyOverlays(mContext, enabled, mCallback);
        mLatch.await(5, TimeUnit.SECONDS);

        verify(mOverlayManager, times(1)).setEnabled(getOverlayPackageId(), enabled,
                mUserHandle);
    }

    @Test
    public void testDefaultEnabled_launchDocumentsUI_shouldSuccess() throws Exception {
        final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        final Activity activity =
                InstrumentationRegistry.getInstrumentation().startActivitySync(intent);

        assertThat(activity).isNotNull();

        if (activity != null) {
            activity.finish();
        }
    }

    private static OverlayInfo createOverlayInfo(String packageName, String targetPackageName,
            boolean enabled) {
        return new OverlayInfo(packageName, targetPackageName, null, null, "",
                enabled ? OverlayInfo.STATE_ENABLED : OverlayInfo.STATE_DISABLED, 0, 0, false);
    }

    private static String getDisabledTargetPackageId() {
        return TEST_DISABLED_PREFIX + TEST_TARGET_PACKAGE;
    }

    private static String getEnabledTargetPackageId() {
        return TEST_ENABLED_PREFIX + TEST_TARGET_PACKAGE;
    }

    private static String getOverlayPackageId() {
        return TEST_DISABLED_PREFIX + TEST_OVERLAY_PACKAGE;
    }

}