summaryrefslogtreecommitdiffstats
path: root/core/java/android/widget/RemoteViewsAdapter.java
blob: 1c0a2bbb0f1e563c8d15d76c6fb3c76d1227a63a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
/*
 * Copyright (C) 2007 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 java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;

import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;

import com.android.internal.widget.IRemoteViewsAdapterConnection;
import com.android.internal.widget.IRemoteViewsFactory;

/**
 * An adapter to a RemoteViewsService which fetches and caches RemoteViews
 * to be later inflated as child views.
 */
/** @hide */
public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback {
    private static final String TAG = "RemoteViewsAdapter";

    // The max number of items in the cache
    private static final int sDefaultCacheSize = 40;
    // The delay (in millis) to wait until attempting to unbind from a service after a request.
    // This ensures that we don't stay continually bound to the service and that it can be destroyed
    // if we need the memory elsewhere in the system.
    private static final int sUnbindServiceDelay = 5000;
    // Type defs for controlling different messages across the main and worker message queues
    private static final int sDefaultMessageType = 0;
    private static final int sUnbindServiceMessageType = 1;

    private final Context mContext;
    private final Intent mIntent;
    private final int mAppWidgetId;
    private LayoutInflater mLayoutInflater;
    private RemoteViewsAdapterServiceConnection mServiceConnection;
    private WeakReference<RemoteAdapterConnectionCallback> mCallback;
    private FixedSizeRemoteViewsCache mCache;

    // A flag to determine whether we should notify data set changed after we connect
    private boolean mNotifyDataSetChangedAfterOnServiceConnected = false;

    // The set of requested views that are to be notified when the associated RemoteViews are
    // loaded.
    private RemoteViewsFrameLayoutRefSet mRequestedViews;

    private HandlerThread mWorkerThread;
    // items may be interrupted within the normally processed queues
    private Handler mWorkerQueue;
    private Handler mMainQueue;

    /**
     * An interface for the RemoteAdapter to notify other classes when adapters
     * are actually connected to/disconnected from their actual services.
     */
    public interface RemoteAdapterConnectionCallback {
        /**
         * @return whether the adapter was set or not.
         */
        public boolean onRemoteAdapterConnected();

        public void onRemoteAdapterDisconnected();
    }

    /**
     * The service connection that gets populated when the RemoteViewsService is
     * bound.  This must be a static inner class to ensure that no references to the outer
     * RemoteViewsAdapter instance is retained (this would prevent the RemoteViewsAdapter from being
     * garbage collected, and would cause us to leak activities due to the caching mechanism for
     * FrameLayouts in the adapter).
     */
    private static class RemoteViewsAdapterServiceConnection extends
            IRemoteViewsAdapterConnection.Stub {
        private boolean mIsConnected;
        private boolean mIsConnecting;
        private WeakReference<RemoteViewsAdapter> mAdapter;
        private IRemoteViewsFactory mRemoteViewsFactory;

        public RemoteViewsAdapterServiceConnection(RemoteViewsAdapter adapter) {
            mAdapter = new WeakReference<RemoteViewsAdapter>(adapter);
        }

        public synchronized void bind(Context context, int appWidgetId, Intent intent) {
            if (!mIsConnecting) {
                try {
                    final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
                    mgr.bindRemoteViewsService(appWidgetId, intent, asBinder());
                    mIsConnecting = true;
                } catch (Exception e) {
                    Log.e("RemoteViewsAdapterServiceConnection", "bind(): " + e.getMessage());
                    mIsConnecting = false;
                    mIsConnected = false;
                }
            }
        }

        public synchronized void unbind(Context context, int appWidgetId, Intent intent) {
            try {
                final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
                mgr.unbindRemoteViewsService(appWidgetId, intent);
                mIsConnecting = false;
            } catch (Exception e) {
                Log.e("RemoteViewsAdapterServiceConnection", "unbind(): " + e.getMessage());
                mIsConnecting = false;
                mIsConnected = false;
            }
        }

        public synchronized void onServiceConnected(IBinder service) {
            mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);

            // Remove any deferred unbind messages
            final RemoteViewsAdapter adapter = mAdapter.get();
            if (adapter == null) return;

            // Queue up work that we need to do for the callback to run
            adapter.mWorkerQueue.post(new Runnable() {
                @Override
                public void run() {
                    if (adapter.mNotifyDataSetChangedAfterOnServiceConnected) {
                        // Handle queued notifyDataSetChanged() if necessary
                        adapter.onNotifyDataSetChanged();
                    } else {
                        IRemoteViewsFactory factory =
                            adapter.mServiceConnection.getRemoteViewsFactory();
                        try {
                            if (!factory.isCreated()) {
                                // We only call onDataSetChanged() if this is the factory was just
                                // create in response to this bind
                                factory.onDataSetChanged();
                            }
                        } catch (Exception e) {
                            Log.e(TAG, "Error notifying factory of data set changed in " +
                                        "onServiceConnected(): " + e.getMessage());

                            // Return early to prevent anything further from being notified
                            // (effectively nothing has changed)
                            return;
                        }

                        // Request meta data so that we have up to date data when calling back to
                        // the remote adapter callback
                        adapter.updateTemporaryMetaData();

                        // Notify the host that we've connected
                        adapter.mMainQueue.post(new Runnable() {
                            @Override
                            public void run() {
                                synchronized (adapter.mCache) {
                                    adapter.mCache.commitTemporaryMetaData();
                                }

                                final RemoteAdapterConnectionCallback callback =
                                    adapter.mCallback.get();
                                if (callback != null) {
                                    callback.onRemoteAdapterConnected();
                                }
                            }
                        });
                    }

                    // Enqueue unbind message
                    adapter.enqueueDeferredUnbindServiceMessage();
                    mIsConnected = true;
                    mIsConnecting = false;
                }
            });
        }

        public synchronized void onServiceDisconnected() {
            mIsConnected = false;
            mIsConnecting = false;
            mRemoteViewsFactory = null;

            // Clear the main/worker queues
            final RemoteViewsAdapter adapter = mAdapter.get();
            if (adapter == null) return;
            
            adapter.mMainQueue.post(new Runnable() {
                @Override
                public void run() {
                    // Dequeue any unbind messages
                    adapter.mMainQueue.removeMessages(sUnbindServiceMessageType);

                    final RemoteAdapterConnectionCallback callback = adapter.mCallback.get();
                    if (callback != null) {
                        callback.onRemoteAdapterDisconnected();
                    }
                }
            });
        }

        public synchronized IRemoteViewsFactory getRemoteViewsFactory() {
            return mRemoteViewsFactory;
        }

        public synchronized boolean isConnected() {
            return mIsConnected;
        }
    }

    /**
     * A FrameLayout which contains a loading view, and manages the re/applying of RemoteViews when
     * they are loaded.
     */
    private class RemoteViewsFrameLayout extends FrameLayout {
        public RemoteViewsFrameLayout(Context context) {
            super(context);
        }

        /**
         * Updates this RemoteViewsFrameLayout depending on the view that was loaded.
         * @param view the RemoteViews that was loaded. If null, the RemoteViews was not loaded
         *             successfully.
         */
        public void onRemoteViewsLoaded(RemoteViews view) {
            try {
                // Remove all the children of this layout first
                removeAllViews();
                addView(view.apply(getContext(), this));
            } catch (Exception e) {
                Log.e(TAG, "Failed to apply RemoteViews.");
            }
        }
    }

    /**
     * Stores the references of all the RemoteViewsFrameLayouts that have been returned by the
     * adapter that have not yet had their RemoteViews loaded.
     */
    private class RemoteViewsFrameLayoutRefSet {
        private HashMap<Integer, LinkedList<RemoteViewsFrameLayout>> mReferences;

        public RemoteViewsFrameLayoutRefSet() {
            mReferences = new HashMap<Integer, LinkedList<RemoteViewsFrameLayout>>();
        }

        /**
         * Adds a new reference to a RemoteViewsFrameLayout returned by the adapter.
         */
        public void add(int position, RemoteViewsFrameLayout layout) {
            final Integer pos = position;
            LinkedList<RemoteViewsFrameLayout> refs;

            // Create the list if necessary
            if (mReferences.containsKey(pos)) {
                refs = mReferences.get(pos);
            } else {
                refs = new LinkedList<RemoteViewsFrameLayout>();
                mReferences.put(pos, refs);
            }

            // Add the references to the list
            refs.add(layout);
        }

        /**
         * Notifies each of the RemoteViewsFrameLayouts associated with a particular position that
         * the associated RemoteViews has loaded.
         */
        public void notifyOnRemoteViewsLoaded(int position, RemoteViews view, int typeId) {
            if (view == null) return;

            final Integer pos = position;
            if (mReferences.containsKey(pos)) {
                // Notify all the references for that position of the newly loaded RemoteViews
                final LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(pos);
                for (final RemoteViewsFrameLayout ref : refs) {
                    ref.onRemoteViewsLoaded(view);
                }
                refs.clear();

                // Remove this set from the original mapping
                mReferences.remove(pos);
            }
        }

        /**
         * Removes all references to all RemoteViewsFrameLayouts returned by the adapter.
         */
        public void clear() {
            // We currently just clear the references, and leave all the previous layouts returned
            // in their default state of the loading view.
            mReferences.clear();
        }
    }

    /**
     * The meta-data associated with the cache in it's current state.
     */
    private class RemoteViewsMetaData {
        int count;
        int viewTypeCount;
        boolean hasStableIds;

        // Used to determine how to construct loading views.  If a loading view is not specified
        // by the user, then we try and load the first view, and use its height as the height for
        // the default loading view.
        RemoteViews mUserLoadingView;
        RemoteViews mFirstView;
        int mFirstViewHeight;

        // A mapping from type id to a set of unique type ids
        private final HashMap<Integer, Integer> mTypeIdIndexMap = new HashMap<Integer, Integer>();

        public RemoteViewsMetaData() {
            reset();
        }

        public void set(RemoteViewsMetaData d) {
            synchronized (d) {
                count = d.count;
                viewTypeCount = d.viewTypeCount;
                hasStableIds = d.hasStableIds;
                setLoadingViewTemplates(d.mUserLoadingView, d.mFirstView);
            }
        }

        public void reset() {
            count = 0;

            // by default there is at least one dummy view type
            viewTypeCount = 1;
            hasStableIds = true;
            mUserLoadingView = null;
            mFirstView = null;
            mFirstViewHeight = 0;
            mTypeIdIndexMap.clear();
        }

        public void setLoadingViewTemplates(RemoteViews loadingView, RemoteViews firstView) {
            mUserLoadingView = loadingView;
            if (firstView != null) {
                mFirstView = firstView;
                mFirstViewHeight = -1;
            }
        }

        public int getMappedViewType(int typeId) {
            if (mTypeIdIndexMap.containsKey(typeId)) {
                return mTypeIdIndexMap.get(typeId);
            } else {
                // We +1 because the loading view always has view type id of 0
                int incrementalTypeId = mTypeIdIndexMap.size() + 1;
                mTypeIdIndexMap.put(typeId, incrementalTypeId);
                return incrementalTypeId;
            }
        }

        private RemoteViewsFrameLayout createLoadingView(int position, View convertView,
                ViewGroup parent) {
            // Create and return a new FrameLayout, and setup the references for this position
            final Context context = parent.getContext();
            RemoteViewsFrameLayout layout = new RemoteViewsFrameLayout(context);

            // Create a new loading view
            synchronized (mCache) {
                if (mUserLoadingView != null) {
                    // A user-specified loading view
                    View loadingView = mUserLoadingView.apply(parent.getContext(), parent);
                    loadingView.setTagInternal(com.android.internal.R.id.rowTypeId, new Integer(0));
                    layout.addView(loadingView);
                } else {
                    // A default loading view
                    // Use the size of the first row as a guide for the size of the loading view
                    if (mFirstViewHeight < 0) {
                        View firstView = mFirstView.apply(parent.getContext(), parent);
                        firstView.measure(
                                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
                        mFirstViewHeight = firstView.getMeasuredHeight();
                        mFirstView = null;
                    }

                    // Compose the loading view text
                    TextView loadingTextView = (TextView) mLayoutInflater.inflate(
                            com.android.internal.R.layout.remote_views_adapter_default_loading_view,
                            layout, false);
                    loadingTextView.setHeight(mFirstViewHeight);
                    loadingTextView.setTag(new Integer(0));

                    layout.addView(loadingTextView);
                }
            }

            return layout;
        }
    }

    /**
     * The meta-data associated with a single item in the cache.
     */
    private class RemoteViewsIndexMetaData {
        int typeId;
        long itemId;
        boolean isRequested;

        public RemoteViewsIndexMetaData(RemoteViews v, long itemId, boolean requested) {
            set(v, itemId, requested);
        }

        public void set(RemoteViews v, long id, boolean requested) {
            itemId = id;
            if (v != null)
                typeId = v.getLayoutId();
            else
                typeId = 0;
            isRequested = requested;
        }
    }

    /**
     *
     */
    private class FixedSizeRemoteViewsCache {
        private static final String TAG = "FixedSizeRemoteViewsCache";

        // The meta data related to all the RemoteViews, ie. count, is stable, etc.
        private RemoteViewsMetaData mMetaData;
        private RemoteViewsMetaData mTemporaryMetaData;

        // The cache/mapping of position to RemoteViewsMetaData.  This set is guaranteed to be
        // greater than or equal to the set of RemoteViews.
        // Note: The reason that we keep this separate from the RemoteViews cache below is that this
        // we still need to be able to access the mapping of position to meta data, without keeping
        // the heavy RemoteViews around.  The RemoteViews cache is trimmed to fixed constraints wrt.
        // memory and size, but this metadata cache will retain information until the data at the
        // position is guaranteed as not being necessary any more (usually on notifyDataSetChanged).
        private HashMap<Integer, RemoteViewsIndexMetaData> mIndexMetaData;

        // The cache of actual RemoteViews, which may be pruned if the cache gets too large, or uses
        // too much memory.
        private HashMap<Integer, RemoteViews> mIndexRemoteViews;

        // The set of indices that have been explicitly requested by the collection view
        private HashSet<Integer> mRequestedIndices;

        // We keep a reference of the last requested index to determine which item to prune the
        // farthest items from when we hit the memory limit
        private int mLastRequestedIndex;

        // The set of indices to load, including those explicitly requested, as well as those
        // determined by the preloading algorithm to be prefetched
        private HashSet<Integer> mLoadIndices;

        // The lower and upper bounds of the preloaded range
        private int mPreloadLowerBound;
        private int mPreloadUpperBound;

        // The bounds of this fixed cache, we will try and fill as many items into the cache up to
        // the maxCount number of items, or the maxSize memory usage.
        // The maxCountSlack is used to determine if a new position in the cache to be loaded is
        // sufficiently ouside the old set, prompting a shifting of the "window" of items to be
        // preloaded.
        private int mMaxCount;
        private int mMaxCountSlack;
        private static final float sMaxCountSlackPercent = 0.75f;
        private static final int sMaxMemoryLimitInBytes = 2 * 1024 * 1024;

        public FixedSizeRemoteViewsCache(int maxCacheSize) {
            mMaxCount = maxCacheSize;
            mMaxCountSlack = Math.round(sMaxCountSlackPercent * (mMaxCount / 2));
            mPreloadLowerBound = 0;
            mPreloadUpperBound = -1;
            mMetaData = new RemoteViewsMetaData();
            mTemporaryMetaData = new RemoteViewsMetaData();
            mIndexMetaData = new HashMap<Integer, RemoteViewsIndexMetaData>();
            mIndexRemoteViews = new HashMap<Integer, RemoteViews>();
            mRequestedIndices = new HashSet<Integer>();
            mLastRequestedIndex = -1;
            mLoadIndices = new HashSet<Integer>();
        }

        public void insert(int position, RemoteViews v, long itemId, boolean isRequested) {
            // Trim the cache if we go beyond the count
            if (mIndexRemoteViews.size() >= mMaxCount) {
                mIndexRemoteViews.remove(getFarthestPositionFrom(position));
            }

            // Trim the cache if we go beyond the available memory size constraints
            int pruneFromPosition = (mLastRequestedIndex > -1) ? mLastRequestedIndex : position;
            while (getRemoteViewsBitmapMemoryUsage() >= sMaxMemoryLimitInBytes) {
                // Note: This is currently the most naive mechanism for deciding what to prune when
                // we hit the memory limit.  In the future, we may want to calculate which index to
                // remove based on both its position as well as it's current memory usage, as well
                // as whether it was directly requested vs. whether it was preloaded by our caching
                // mechanism.
                mIndexRemoteViews.remove(getFarthestPositionFrom(pruneFromPosition));
            }

            // Update the metadata cache
            if (mIndexMetaData.containsKey(position)) {
                final RemoteViewsIndexMetaData metaData = mIndexMetaData.get(position);
                metaData.set(v, itemId, isRequested);
            } else {
                mIndexMetaData.put(position, new RemoteViewsIndexMetaData(v, itemId, isRequested));
            }
            mIndexRemoteViews.put(position, v);
        }

        public RemoteViewsMetaData getMetaData() {
            return mMetaData;
        }
        public RemoteViewsMetaData getTemporaryMetaData() {
            return mTemporaryMetaData;
        }
        public RemoteViews getRemoteViewsAt(int position) {
            if (mIndexRemoteViews.containsKey(position)) {
                return mIndexRemoteViews.get(position);
            }
            return null;
        }
        public RemoteViewsIndexMetaData getMetaDataAt(int position) {
            if (mIndexMetaData.containsKey(position)) {
                return mIndexMetaData.get(position);
            }
            return null;
        }

        public void commitTemporaryMetaData() {
            synchronized (mTemporaryMetaData) {
                synchronized (mMetaData) {
                    mMetaData.set(mTemporaryMetaData);
                }
            }
        }

        private int getRemoteViewsBitmapMemoryUsage() {
            // Calculate the memory usage of all the RemoteViews bitmaps being cached
            int mem = 0;
            for (Integer i : mIndexRemoteViews.keySet()) {
                final RemoteViews v = mIndexRemoteViews.get(i);
                if (v != null) {
                    mem += v.estimateBitmapMemoryUsage();
                }
            }
            return mem;
        }
        private int getFarthestPositionFrom(int pos) {
            // Find the index farthest away and remove that
            int maxDist = 0;
            int maxDistIndex = -1;
            int maxDistNonRequested = 0;
            int maxDistIndexNonRequested = -1;
            for (int i : mIndexRemoteViews.keySet()) {
                int dist = Math.abs(i-pos);
                if (dist > maxDistNonRequested && !mIndexMetaData.get(i).isRequested) {
                    // maxDistNonRequested/maxDistIndexNonRequested will store the index of the
                    // farthest non-requested position
                    maxDistIndexNonRequested = i;
                    maxDistNonRequested = dist;
                }
                if (dist > maxDist) {
                    // maxDist/maxDistIndex will store the index of the farthest position
                    // regardless of whether it was directly requested or not
                    maxDistIndex = i;
                    maxDist = dist;
                }
            }
            if (maxDistIndexNonRequested > -1) {
                return maxDistIndexNonRequested;
            }
            return maxDistIndex;
        }

        public void queueRequestedPositionToLoad(int position) {
            mLastRequestedIndex = position;
            synchronized (mLoadIndices) {
                mRequestedIndices.add(position);
                mLoadIndices.add(position);
            }
        }
        public boolean queuePositionsToBePreloadedFromRequestedPosition(int position) {
            // Check if we need to preload any items
            if (mPreloadLowerBound <= position && position <= mPreloadUpperBound) {
                int center = (mPreloadUpperBound + mPreloadLowerBound) / 2;
                if (Math.abs(position - center) < mMaxCountSlack) {
                    return false;
                }
            }

            int count = 0;
            synchronized (mMetaData) {
                count = mMetaData.count;
            }
            synchronized (mLoadIndices) {
                mLoadIndices.clear();

                // Add all the requested indices
                mLoadIndices.addAll(mRequestedIndices);

                // Add all the preload indices
                int halfMaxCount = mMaxCount / 2;
                mPreloadLowerBound = position - halfMaxCount;
                mPreloadUpperBound = position + halfMaxCount;
                int effectiveLowerBound = Math.max(0, mPreloadLowerBound);
                int effectiveUpperBound = Math.min(mPreloadUpperBound, count - 1);
                for (int i = effectiveLowerBound; i <= effectiveUpperBound; ++i) {
                    mLoadIndices.add(i);
                }

                // But remove all the indices that have already been loaded and are cached
                mLoadIndices.removeAll(mIndexRemoteViews.keySet());
            }
            return true;
        }
        /** Returns the next index to load, and whether that index was directly requested or not */
        public int[] getNextIndexToLoad() {
            // We try and prioritize items that have been requested directly, instead
            // of items that are loaded as a result of the caching mechanism
            synchronized (mLoadIndices) {
                // Prioritize requested indices to be loaded first
                if (!mRequestedIndices.isEmpty()) {
                    Integer i = mRequestedIndices.iterator().next();
                    mRequestedIndices.remove(i);
                    mLoadIndices.remove(i);
                    return new int[]{i.intValue(), 1};
                }

                // Otherwise, preload other indices as necessary
                if (!mLoadIndices.isEmpty()) {
                    Integer i = mLoadIndices.iterator().next();
                    mLoadIndices.remove(i);
                    return new int[]{i.intValue(), 0};
                }

                return new int[]{-1, 0};
            }
        }

        public boolean containsRemoteViewAt(int position) {
            return mIndexRemoteViews.containsKey(position);
        }
        public boolean containsMetaDataAt(int position) {
            return mIndexMetaData.containsKey(position);
        }

        public void reset() {
            // Note: We do not try and reset the meta data, since that information is still used by
            // collection views to validate it's own contents (and will be re-requested if the data
            // is invalidated through the notifyDataSetChanged() flow).

            mPreloadLowerBound = 0;
            mPreloadUpperBound = -1;
            mLastRequestedIndex = -1;
            mIndexRemoteViews.clear();
            mIndexMetaData.clear();
            synchronized (mLoadIndices) {
                mRequestedIndices.clear();
                mLoadIndices.clear();
            }
        }
    }

    public RemoteViewsAdapter(Context context, Intent intent, RemoteAdapterConnectionCallback callback) {
        mContext = context;
        mIntent = intent;
        mAppWidgetId = intent.getIntExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID, -1);
        mLayoutInflater = LayoutInflater.from(context);
        if (mIntent == null) {
            throw new IllegalArgumentException("Non-null Intent must be specified.");
        }
        mRequestedViews = new RemoteViewsFrameLayoutRefSet();

        // Strip the previously injected app widget id from service intent
        if (intent.hasExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID)) {
            intent.removeExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID);
        }

        // Initialize the worker thread
        mWorkerThread = new HandlerThread("RemoteViewsCache-loader");
        mWorkerThread.start();
        mWorkerQueue = new Handler(mWorkerThread.getLooper());
        mMainQueue = new Handler(Looper.myLooper(), this);

        // Initialize the cache and the service connection on startup
        mCache = new FixedSizeRemoteViewsCache(sDefaultCacheSize);
        mCallback = new WeakReference<RemoteAdapterConnectionCallback>(callback);
        mServiceConnection = new RemoteViewsAdapterServiceConnection(this);
        requestBindService();
    }

    private void loadNextIndexInBackground() {
        mWorkerQueue.post(new Runnable() {
            @Override
            public void run() {
                if (mServiceConnection.isConnected()) {
                    // Get the next index to load
                    int position = -1;
                    boolean isRequested = false;
                    synchronized (mCache) {
                        int[] res = mCache.getNextIndexToLoad();
                        position = res[0];
                        isRequested = res[1] > 0;
                    }
                    if (position > -1) {
                        // Load the item, and notify any existing RemoteViewsFrameLayouts
                        updateRemoteViews(position, isRequested);

                        // Queue up for the next one to load
                        loadNextIndexInBackground();
                    } else {
                        // No more items to load, so queue unbind
                        enqueueDeferredUnbindServiceMessage();
                    }
                }
            }
        });
    }

    private void processException(String method, Exception e) {
        Log.e("RemoteViewsAdapter", "Error in " + method + ": " + e.getMessage());

        // If we encounter a crash when updating, we should reset the metadata & cache and trigger
        // a notifyDataSetChanged to update the widget accordingly
        final RemoteViewsMetaData metaData = mCache.getMetaData();
        synchronized (metaData) {
            metaData.reset();
        }
        synchronized (mCache) {
            mCache.reset();
        }
        mMainQueue.post(new Runnable() {
            @Override
            public void run() {
                superNotifyDataSetChanged();
            }
        });
    }

    private void updateTemporaryMetaData() {
        IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();

        try {
            // get the properties/first view (so that we can use it to
            // measure our dummy views)
            boolean hasStableIds = factory.hasStableIds();
            int viewTypeCount = factory.getViewTypeCount();
            int count = factory.getCount();
            RemoteViews loadingView = factory.getLoadingView();
            RemoteViews firstView = null;
            if ((count > 0) && (loadingView == null)) {
                firstView = factory.getViewAt(0);
            }
            final RemoteViewsMetaData tmpMetaData = mCache.getTemporaryMetaData();
            synchronized (tmpMetaData) {
                tmpMetaData.hasStableIds = hasStableIds;
                // We +1 because the base view type is the loading view
                tmpMetaData.viewTypeCount = viewTypeCount + 1;
                tmpMetaData.count = count;
                tmpMetaData.setLoadingViewTemplates(loadingView, firstView);
            }
        } catch (Exception e) {
            processException("updateMetaData", e);
        }
    }

    private void updateRemoteViews(final int position, boolean isRequested) {
        if (!mServiceConnection.isConnected()) return;
        IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();

        // Load the item information from the remote service
        RemoteViews remoteViews = null;
        long itemId = 0;
        try {
            remoteViews = factory.getViewAt(position);
            itemId = factory.getItemId(position);
        } catch (Exception e) {
            Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage());

            // Return early to prevent additional work in re-centering the view cache, and
            // swapping from the loading view
            return;
        }

        if (remoteViews == null) {
            // If a null view was returned, we break early to prevent it from getting
            // into our cache and causing problems later. The effect is that the child  at this
            // position will remain as a loading view until it is updated.
            Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + " null RemoteViews " +
                    "returned from RemoteViewsFactory.");
            return;
        }
        synchronized (mCache) {
            // Cache the RemoteViews we loaded
            mCache.insert(position, remoteViews, itemId, isRequested);

            // Notify all the views that we have previously returned for this index that
            // there is new data for it.
            final RemoteViews rv = remoteViews;
            final int typeId = mCache.getMetaDataAt(position).typeId;
            mMainQueue.post(new Runnable() {
                @Override
                public void run() {
                    mRequestedViews.notifyOnRemoteViewsLoaded(position, rv, typeId);
                }
            });
        }
    }

    public Intent getRemoteViewsServiceIntent() {
        return mIntent;
    }

    public int getCount() {
        final RemoteViewsMetaData metaData = mCache.getMetaData();
        synchronized (metaData) {
            return metaData.count;
        }
    }

    public Object getItem(int position) {
        // Disallow arbitrary object to be associated with an item for the time being
        return null;
    }

    public long getItemId(int position) {
        synchronized (mCache) {
            if (mCache.containsMetaDataAt(position)) {
                return mCache.getMetaDataAt(position).itemId;
            }
            return 0;
        }
    }

    public int getItemViewType(int position) {
        int typeId = 0;
        synchronized (mCache) {
            if (mCache.containsMetaDataAt(position)) {
                typeId = mCache.getMetaDataAt(position).typeId;
            } else {
                return 0;
            }
        }

        final RemoteViewsMetaData metaData = mCache.getMetaData();
        synchronized (metaData) {
            return metaData.getMappedViewType(typeId);
        }
    }

    /**
     * Returns the item type id for the specified convert view.  Returns -1 if the convert view
     * is invalid.
     */
    private int getConvertViewTypeId(View convertView) {
        int typeId = -1;
        if (convertView != null) {
            Object tag = convertView.getTag(com.android.internal.R.id.rowTypeId);
            if (tag != null) {
                typeId = (Integer) tag;
            }
        }
        return typeId;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        // "Request" an index so that we can queue it for loading, initiate subsequent
        // preloading, etc.
        synchronized (mCache) {
            boolean isInCache = mCache.containsRemoteViewAt(position);
            boolean isConnected = mServiceConnection.isConnected();
            boolean hasNewItems = false;

            if (!isInCache && !isConnected) {
                // Requesting bind service will trigger a super.notifyDataSetChanged(), which will
                // in turn trigger another request to getView()
                requestBindService();
            } else {
                // Queue up other indices to be preloaded based on this position
                hasNewItems = mCache.queuePositionsToBePreloadedFromRequestedPosition(position);
            }

            if (isInCache) {
                View convertViewChild = null;
                int convertViewTypeId = 0;
                RemoteViewsFrameLayout layout = null;

                if (convertView instanceof RemoteViewsFrameLayout) {
                    layout = (RemoteViewsFrameLayout) convertView;
                    convertViewChild = layout.getChildAt(0);
                    convertViewTypeId = getConvertViewTypeId(convertViewChild);
                }

                // Second, we try and retrieve the RemoteViews from the cache, returning a loading
                // view and queueing it to be loaded if it has not already been loaded.
                Context context = parent.getContext();
                RemoteViews rv = mCache.getRemoteViewsAt(position);
                RemoteViewsIndexMetaData indexMetaData = mCache.getMetaDataAt(position);
                indexMetaData.isRequested = true;
                int typeId = indexMetaData.typeId;

                // Reuse the convert view where possible
                if (layout != null) {
                    if (convertViewTypeId == typeId) {
                        rv.reapply(context, convertViewChild);
                        return layout;
                    }
                    layout.removeAllViews();
                } else {
                    layout = new RemoteViewsFrameLayout(context);
                }

                // Otherwise, create a new view to be returned
                View newView = rv.apply(context, parent);
                newView.setTagInternal(com.android.internal.R.id.rowTypeId, new Integer(typeId));
                layout.addView(newView);
                if (hasNewItems) loadNextIndexInBackground();

                return layout;
            } else {
                // If the cache does not have the RemoteViews at this position, then create a
                // loading view and queue the actual position to be loaded in the background
                RemoteViewsFrameLayout loadingView = null;
                final RemoteViewsMetaData metaData = mCache.getMetaData();
                synchronized (metaData) {
                    loadingView = metaData.createLoadingView(position, convertView, parent);
                }

                mRequestedViews.add(position, loadingView);
                mCache.queueRequestedPositionToLoad(position);
                loadNextIndexInBackground();

                return loadingView;
            }
        }
    }

    public int getViewTypeCount() {
        final RemoteViewsMetaData metaData = mCache.getMetaData();
        synchronized (metaData) {
            return metaData.viewTypeCount;
        }
    }

    public boolean hasStableIds() {
        final RemoteViewsMetaData metaData = mCache.getMetaData();
        synchronized (metaData) {
            return metaData.hasStableIds;
        }
    }

    public boolean isEmpty() {
        return getCount() <= 0;
    }


    private void onNotifyDataSetChanged() {
        // Complete the actual notifyDataSetChanged() call initiated earlier
        IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
        try {
            factory.onDataSetChanged();
        } catch (Exception e) {
            Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());

            // Return early to prevent from further being notified (since nothing has
            // changed)
            return;
        }

        // Flush the cache so that we can reload new items from the service
        synchronized (mCache) {
            mCache.reset();
        }

        // Re-request the new metadata (only after the notification to the factory)
        updateTemporaryMetaData();

        // Propagate the notification back to the base adapter
        mMainQueue.post(new Runnable() {
            @Override
            public void run() {
                synchronized (mCache) {
                    mCache.commitTemporaryMetaData();
                }

                superNotifyDataSetChanged();
                enqueueDeferredUnbindServiceMessage();
            }
        });

        // Reset the notify flagflag
        mNotifyDataSetChangedAfterOnServiceConnected = false;
    }

    public void notifyDataSetChanged() {
        // Dequeue any unbind messages
        mMainQueue.removeMessages(sUnbindServiceMessageType);

        // If we are not connected, queue up the notifyDataSetChanged to be handled when we do
        // connect
        if (!mServiceConnection.isConnected()) {
            if (mNotifyDataSetChangedAfterOnServiceConnected) {
                return;
            }

            mNotifyDataSetChangedAfterOnServiceConnected = true;
            requestBindService();
            return;
        }

        mWorkerQueue.post(new Runnable() {
            @Override
            public void run() {
                onNotifyDataSetChanged();
            }
        });
    }

    void superNotifyDataSetChanged() {
        super.notifyDataSetChanged();
    }

    @Override
    public boolean handleMessage(Message msg) {
        boolean result = false;
        switch (msg.what) {
        case sUnbindServiceMessageType:
            if (mServiceConnection.isConnected()) {
                mServiceConnection.unbind(mContext, mAppWidgetId, mIntent);
            }
            result = true;
            break;
        default:
            break;
        }
        return result;
    }

    private void enqueueDeferredUnbindServiceMessage() {
        // Remove any existing deferred-unbind messages
        mMainQueue.removeMessages(sUnbindServiceMessageType);
        mMainQueue.sendEmptyMessageDelayed(sUnbindServiceMessageType, sUnbindServiceDelay);
    }

    private boolean requestBindService() {
        // Try binding the service (which will start it if it's not already running)
        if (!mServiceConnection.isConnected()) {
            mServiceConnection.bind(mContext, mAppWidgetId, mIntent);
        }

        // Remove any existing deferred-unbind messages
        mMainQueue.removeMessages(sUnbindServiceMessageType);
        return mServiceConnection.isConnected();
    }
}