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

Commit 3c78f558 authored by Bill Lin's avatar Bill Lin
Browse files

Runtime apply overlay by ThemeOverlayManager

a.Runtime overlay package should not be static
  since it's unable to FOTA upgrade
b.Implement ThemeOverlayManager to manage overlay packages

Test: atest testOverlayPackagesForDocumentsUI_shouldBeNonStatic
      can ensure installed RRO for Docsui is Non-Static
Test: atest DocumentsUIGoogleTests
Test: atest OverlayableTest
Test: manual check UI visual

Bug: 128689309
Bug: 131331107
Bug: 132671058
Bug: 132759377
Bug: 132783782
Bug: 132888127

Change-Id: I689894e226dee8266b523af25606e0100a224cd5
parent 97ce4ce5
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;
    }

}