| /* |
| * Copyright (C) 2017 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 android.widget; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.mockito.Mockito.any; |
| import static org.mockito.Mockito.anyInt; |
| import static org.mockito.Mockito.mock; |
| 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.IServiceConnection; |
| import android.appwidget.AppWidgetManager; |
| import android.content.Context; |
| import android.content.ContextWrapper; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.database.DataSetObserver; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.support.test.InstrumentationRegistry; |
| import android.support.test.filters.SmallTest; |
| import android.support.test.runner.AndroidJUnit4; |
| import android.view.View; |
| |
| import com.android.frameworks.coretests.R; |
| import com.android.internal.widget.IRemoteViewsFactory; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| |
| import java.util.ArrayList; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Supplier; |
| |
| /** |
| * Tests for RemoteViewsAdapter. |
| */ |
| @RunWith(AndroidJUnit4.class) |
| @SmallTest |
| public class RemoteViewsAdapterTest { |
| |
| @Mock AppWidgetManager mAppWidgetManager; |
| @Mock IServiceConnection mIServiceConnection; |
| @Mock RemoteViewsAdapter.RemoteAdapterConnectionCallback mCallback; |
| |
| private Handler mMainHandler; |
| private TestContext mContext; |
| |
| @Before |
| public void setup() { |
| MockitoAnnotations.initMocks(this); |
| when(mAppWidgetManager |
| .bindRemoteViewsService(any(), anyInt(), any(), any(), anyInt())).thenReturn(true); |
| mContext = new TestContext(); |
| mMainHandler = new Handler(Looper.getMainLooper()); |
| } |
| |
| @Test |
| public void onRemoteAdapterConnected_after_metadata_loaded() throws Throwable { |
| RemoteViewsAdapter adapter = getOnUiThread( |
| () -> new RemoteViewsAdapter(mContext, new DistinctIntent(), mCallback, false)); |
| assertFalse(adapter.isDataReady()); |
| |
| assertNotNull(mContext.conn.get()); |
| |
| ViewsFactory factory = new ViewsFactory(1); |
| mContext.sendConnect(factory); |
| |
| waitOnHandler(mMainHandler); |
| verify(mCallback, never()).onRemoteAdapterConnected(); |
| |
| factory.loadingView.set(createViews("loading")); |
| waitOnHandler(mContext.handler.get()); |
| waitOnHandler(mMainHandler); |
| verify(mCallback, times(1)).onRemoteAdapterConnected(); |
| |
| assertEquals((Integer) 1, getOnUiThread(adapter::getCount)); |
| |
| // Service is unbound |
| assertTrue(isUnboundOrScheduled()); |
| } |
| |
| @Test |
| public void viewReplaced_after_mainView_loaded() throws Throwable { |
| RemoteViewsAdapter adapter = getOnUiThread( |
| () -> new RemoteViewsAdapter(mContext, new DistinctIntent(), mCallback, false)); |
| |
| ViewsFactory factory = new ViewsFactory(1); |
| factory.loadingView.set(createViews("loading")); |
| mContext.sendConnect(factory); |
| |
| waitOnHandler(mContext.handler.get()); |
| waitOnHandler(mMainHandler); |
| |
| // Returned view contains the loading text |
| View view = getOnUiThread(() -> adapter.getView(0, null, new FrameLayout(mContext))); |
| ArrayList<View> search = new ArrayList<>(); |
| view.findViewsWithText(search, "loading", View.FIND_VIEWS_WITH_TEXT); |
| assertEquals(1, search.size()); |
| |
| // Send the final remoteViews |
| factory.views[0].set(createViews("updated")); |
| waitOnHandler(mContext.handler.get()); |
| waitOnHandler(mMainHandler); |
| |
| // Existing view got updated with new text |
| search.clear(); |
| view.findViewsWithText(search, "loading", View.FIND_VIEWS_WITH_TEXT); |
| assertTrue(search.isEmpty()); |
| view.findViewsWithText(search, "updated", View.FIND_VIEWS_WITH_TEXT); |
| assertEquals(1, search.size()); |
| |
| // Service is unbound |
| assertTrue(isUnboundOrScheduled()); |
| } |
| |
| @Test |
| public void notifyDataSetChanged_deferred() throws Throwable { |
| RemoteViewsAdapter adapter = getOnUiThread( |
| () -> new RemoteViewsAdapter(mContext, new DistinctIntent(), mCallback, false)); |
| |
| ViewsFactory factory = new ViewsFactory(1); |
| factory.loadingView.set(createViews("loading")); |
| mContext.sendConnect(factory); |
| |
| waitOnHandler(mContext.handler.get()); |
| waitOnHandler(mMainHandler); |
| assertEquals((Integer) 1, getOnUiThread(adapter::getCount)); |
| |
| // Reset the loading view so that next refresh is blocked |
| factory.loadingView = new LockedValue<>(); |
| factory.mCount = 3; |
| DataSetObserver observer = mock(DataSetObserver.class); |
| getOnUiThread(() -> { |
| adapter.registerDataSetObserver(observer); |
| adapter.notifyDataSetChanged(); |
| return null; |
| }); |
| |
| waitOnHandler(mMainHandler); |
| // Still giving the old values |
| verify(observer, never()).onChanged(); |
| assertEquals((Integer) 1, getOnUiThread(adapter::getCount)); |
| |
| factory.loadingView.set(createViews("refreshed")); |
| waitOnHandler(mContext.handler.get()); |
| waitOnHandler(mMainHandler); |
| |
| // When the service returns new data, UI is updated. |
| verify(observer, times(1)).onChanged(); |
| assertEquals((Integer) 3, getOnUiThread(adapter::getCount)); |
| |
| // Service is unbound |
| assertTrue(isUnboundOrScheduled()); |
| } |
| |
| @Test |
| public void serviceDisconnected_before_getView() throws Throwable { |
| RemoteViewsAdapter adapter = getOnUiThread( |
| () -> new RemoteViewsAdapter(mContext, new DistinctIntent(), mCallback, false)); |
| |
| ViewsFactory factory = new ViewsFactory(1); |
| factory.loadingView.set(createViews("loading")); |
| mContext.sendConnect(factory); |
| |
| waitOnHandler(mContext.handler.get()); |
| waitOnHandler(mMainHandler); |
| verify(mCallback, times(1)).onRemoteAdapterConnected(); |
| assertEquals((Integer) 1, getOnUiThread(adapter::getCount)); |
| |
| // Unbind the service |
| ServiceConnection conn = mContext.conn.get(); |
| getOnHandler(mContext.handler.get(), () -> { |
| conn.onServiceDisconnected(null); |
| return null; |
| }); |
| |
| // Returned view contains the loading text |
| View view = getOnUiThread(() -> adapter.getView(0, null, new FrameLayout(mContext))); |
| ArrayList<View> search = new ArrayList<>(); |
| view.findViewsWithText(search, "loading", View.FIND_VIEWS_WITH_TEXT); |
| assertEquals(1, search.size()); |
| |
| // Unbind is not scheduled |
| assertFalse(isUnboundOrScheduled()); |
| |
| mContext.sendConnect(factory); |
| waitOnHandler(mContext.handler.get()); |
| waitOnHandler(mMainHandler); |
| verify(mCallback, times(2)).onRemoteAdapterConnected(); |
| } |
| |
| private RemoteViews createViews(String text) { |
| RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.remote_views_text); |
| views.setTextViewText(R.id.text, text); |
| return views; |
| } |
| |
| private <T> T getOnUiThread(Supplier<T> supplier) throws Throwable { |
| return getOnHandler(mMainHandler, supplier); |
| } |
| |
| private boolean isUnboundOrScheduled() throws Throwable { |
| Handler handler = mContext.handler.get(); |
| return getOnHandler(handler, () -> mContext.boundCount == 0 |
| || handler.hasMessages(RemoteViewsAdapter.MSG_UNBIND_SERVICE)); |
| } |
| |
| private static <T> T getOnHandler(Handler handler, Supplier<T> supplier) throws Throwable { |
| LockedValue<T> result = new LockedValue<>(); |
| handler.post(() -> result.set(supplier.get())); |
| return result.get(); |
| } |
| |
| private class TestContext extends ContextWrapper { |
| |
| public final LockedValue<ServiceConnection> conn = new LockedValue<>(); |
| public final LockedValue<Handler> handler = new LockedValue<>(); |
| public int boundCount; |
| |
| TestContext() { |
| super(InstrumentationRegistry.getContext()); |
| } |
| |
| @Override |
| public void unbindService(ServiceConnection conn) { |
| boundCount--; |
| } |
| |
| @Override |
| public Object getSystemService(String name) { |
| if (Context.APPWIDGET_SERVICE.equals(name)) { |
| return mAppWidgetManager; |
| } |
| return super.getSystemService(name); |
| } |
| |
| @Override |
| public IServiceConnection getServiceDispatcher( |
| ServiceConnection conn, Handler handler, int flags) { |
| this.conn.set(conn); |
| this.handler.set(handler); |
| boundCount++; |
| return mIServiceConnection; |
| } |
| |
| @Override |
| public Context getApplicationContext() { |
| return this; |
| } |
| |
| public void sendConnect(ViewsFactory factory) throws Exception { |
| ServiceConnection connection = conn.get(); |
| handler.get().post(() -> connection.onServiceConnected(null, factory.asBinder())); |
| } |
| } |
| |
| private static void waitOnHandler(Handler handler) throws Exception { |
| CountDownLatch latch = new CountDownLatch(1); |
| handler.post(() -> latch.countDown()); |
| latch.await(20, TimeUnit.SECONDS); |
| } |
| |
| private static class ViewsFactory extends IRemoteViewsFactory.Stub { |
| |
| public LockedValue<RemoteViews> loadingView = new LockedValue<>(); |
| public LockedValue<RemoteViews>[] views; |
| |
| private int mCount; |
| |
| ViewsFactory(int count) { |
| mCount = count; |
| views = new LockedValue[count]; |
| for (int i = 0; i < count; i++) { |
| views[i] = new LockedValue<>(); |
| } |
| } |
| |
| @Override |
| public void onDataSetChanged() {} |
| |
| @Override |
| public void onDataSetChangedAsync() { } |
| |
| @Override |
| public void onDestroy(Intent intent) { } |
| |
| @Override |
| public int getCount() throws RemoteException { |
| return mCount; |
| } |
| |
| @Override |
| public RemoteViews getViewAt(int position) throws RemoteException { |
| try { |
| return views[position].get(); |
| } catch (Exception e) { |
| throw new RemoteException(e.getMessage()); |
| } |
| } |
| |
| @Override |
| public RemoteViews getLoadingView() throws RemoteException { |
| try { |
| return loadingView.get(); |
| } catch (Exception e) { |
| throw new RemoteException(e.getMessage()); |
| } |
| } |
| |
| @Override |
| public int getViewTypeCount() { |
| return 1; |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return position; |
| } |
| |
| @Override |
| public boolean hasStableIds() { |
| return false; |
| } |
| |
| @Override |
| public boolean isCreated() { |
| return false; |
| } |
| } |
| |
| private static class DistinctIntent extends Intent { |
| |
| @Override |
| public boolean filterEquals(Intent other) { |
| return false; |
| } |
| } |
| |
| private static class LockedValue<T> { |
| |
| private final CountDownLatch mLatch = new CountDownLatch(1); |
| private T mValue; |
| |
| public void set(T value) { |
| mValue = value; |
| mLatch.countDown(); |
| } |
| |
| public T get() throws Exception { |
| mLatch.await(10, TimeUnit.SECONDS); |
| return mValue; |
| } |
| } |
| } |